package com.googlecode.cpbi;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.packageadmin.PackageAdmin;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;

import com.googlecode.cpbi.util.BundleManifest;
import com.googlecode.cpbi.util.ClassLoaderUtils;
import com.googlecode.cpbi.util.FileUtils;
import com.googlecode.cpbi.util.JarUtils;
import com.googlecode.cpbi.util.OsgiUtils;
import com.googlecode.cpbi.util.PackageUtils;

/**
 * @author Jeremy Wales
 */
public final class ClassPathBundleInstaller implements Runnable {
	public static final Log LOG = LogFactory.getLog(ClassPathBundleInstaller.class);
	private static final String ALL_FILES = "**";

	private final Map exportedAndUsedPackages = new TreeMap();
	private final Set classPathBundles = new HashSet();
	private final BundleContext bundleContext;
	private final PackageAdmin packageAdmin;
	private final String[] excludedPackagePatterns;

	public ClassPathBundleInstaller(BundleContext bundleContext) {
		this.bundleContext = bundleContext;
		this.packageAdmin = OsgiUtils.getPackageAdminFrom(bundleContext);
		this.excludedPackagePatterns = OsgiUtils.getBootDelegationPackagePatternsFrom(bundleContext);
	}

	public void run() {
		URLClassLoader systemClassLoader = ClassLoaderUtils.getSystemClassLoader();
		URL[] classPathUrls = systemClassLoader.getURLs();
		for (int i = 0; i < classPathUrls.length; i++) {
			process(classPathUrls[i]);
		}
		installClassPathExportingSystemBundleFragment();
		resolveClassPathBundles();
	}

	private void resolveClassPathBundles() {
		packageAdmin.resolveBundles((Bundle[]) classPathBundles.toArray(new Bundle[classPathBundles.size()]));
	}

	private void process(URL location) {
		try {
			if (isDirectory(location)) {
				addToExportsThePackagesUnder(location);
			} else {
				processPotentialJarAt(location);
			}
		} catch (IOException e) {
			LOG.error("Could not access resources from '" + location + "'.", e);
		}
	}

	private boolean isDirectory(URL location) {
		try {
			return FileUtils.getFileAt(location).isDirectory();
		} catch (IOException e) {
			return false;
		}
	}

	private void addToExportsThePackagesUnder(URL directory) throws IOException {
		Resource[] resources = FileUtils.getResourcesMatching(directory + ALL_FILES);
		for (int i = 0; i < resources.length; i++) {
			Resource resource = resources[i];
			if (!resource.getFile().isDirectory()) {
				addToExportsThePackageOf(resource, directory);
			}
		}
	}

	private void addToExportsThePackageOf(Resource resource, URL resourceSource) throws IOException {
		String relativeName = FileUtils.getRelativePathOf(resource.getURL(), resourceSource);
		String exportedPackage = PackageUtils.getPackageNameOf(relativeName);
		if (!PackageUtils.matches(exportedPackage, excludedPackagePatterns)) {
			Set usedPackages = createOrAccessUsedPackagesOf(exportedPackage);
			if (relativeName.endsWith(".class")) {
				String[] excluded = StringUtils.addStringToArray(excludedPackagePatterns, exportedPackage);
				usedPackages.addAll(PackageUtils.getPackageDependenciesOf(resource, excluded));
			}
		}
	}

	private Set createOrAccessUsedPackagesOf(String exportPackage) {
		Set usedPackages = (Set) exportedAndUsedPackages.get(exportPackage);
		if (usedPackages == null) {
			exportedAndUsedPackages.put(exportPackage, usedPackages = new TreeSet());
		}
		return usedPackages;
	}

	private void processPotentialJarAt(URL location) throws IOException {
		JarInputStream jarInputStream = JarUtils.NOT_A_JAR;
		jarInputStream = JarUtils.getJarInputStreamFrom(location);
		if (JarUtils.isJar(jarInputStream)) {
			if (OsgiUtils.isBundle(jarInputStream, location)) {
				// JarInputStream does not allow normal reading of bytes, only of entries
				FileUtils.close(jarInputStream, location);
				Bundle installedBundle = OsgiUtils.tryToInstallBundleFrom(location.toString(), bundleContext);
				if (installedBundle != OsgiUtils.UNINSTALLED_BUNDLE) classPathBundles.add(installedBundle);
			} else {
				try {
					addToExportsThePackagesInJarAt(location, jarInputStream);
				} finally {
					FileUtils.close(jarInputStream, location);
				}
			}
		}
	}

	private void addToExportsThePackagesInJarAt(URL location, JarInputStream jarInputStream) throws IOException {
		JarEntry jarEntry = null;
		while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
			if (!jarEntry.isDirectory()) {
				addToExportsThePackageOf(JarUtils.getJarEntryAt(location, jarEntry.getName()), location);
			}
		}
	}

	private void installClassPathExportingSystemBundleFragment() {
		exportedAndUsedPackages.remove("");
		BundleManifest manifest = new BundleManifest();
		String symbolicName = getClass().getPackage().getName() + ".export";
		manifest.setSymbolicName(symbolicName);
		manifest.setFragmentHost(Constants.SYSTEM_BUNDLE_SYMBOLICNAME);
		manifest.setExportPackage(getExportPackageValue());
		// TODO Find out why even optional imports cannot be added to already-resolved fragment host bundles in Equinox
		manifest.setImportPackage(getImportPackageValue());
		InputStream bundleInputStream = OsgiUtils.createEmptyBundleAround(manifest.getManifest());
		classPathBundles.add(OsgiUtils.installBundleFrom(bundleInputStream, "bundle:" + symbolicName, bundleContext));
	}

	private String getExportPackageValue() {
		StringBuffer exportPackageValue = new StringBuffer();
		Set exportedPackages = exportedAndUsedPackages.keySet();
		int index = 0;
		for (Iterator iterator = exportedPackages.iterator(); iterator.hasNext();) {
			String exportedPackage = (String) iterator.next();
			exportPackageValue.append(exportedPackage);
			appendUsedPackages((Set) exportedAndUsedPackages.get(exportedPackage), exportPackageValue);
			if (++index < exportedPackages.size()) exportPackageValue.append(OsgiUtils.PACKAGE_DELIMITER);
		}
		return exportPackageValue.toString();
	}

	private void appendUsedPackages(Set usedPackages, StringBuffer exportPackageValue) {
		if (!usedPackages.isEmpty()) {
			exportPackageValue.append(";uses:=\"");
			int index = 0;
			for (Iterator iterator = usedPackages.iterator(); iterator.hasNext();) {
				exportPackageValue.append((String) iterator.next());
				if (++index < usedPackages.size()) exportPackageValue.append(OsgiUtils.PACKAGE_DELIMITER);
			}
			exportPackageValue.append('"');
		}
	}

	private String getImportPackageValue() {
		Set importedPackages = deriveImportsFromExports();
		removePotentiallyConflictingImportsFrom(importedPackages);
		StringBuffer importPackageValue = new StringBuffer();
		int index = 0;
		for (Iterator iterator = importedPackages.iterator(); iterator.hasNext();) {
			importPackageValue.append(iterator.next()).append(OsgiUtils.ATTRIBUTE_DELIMITER);
			importPackageValue.append(Constants.RESOLUTION_DIRECTIVE).append(":=").append(Constants.RESOLUTION_OPTIONAL);
			if (++index < importedPackages.size()) importPackageValue.append(OsgiUtils.PACKAGE_DELIMITER);
		}
		return importPackageValue.toString();
	}

	private Set deriveImportsFromExports() {
		Set importedPackages = new TreeSet();
		Set exportedPackages = exportedAndUsedPackages.keySet();
		importedPackages.addAll(exportedPackages);
		for (Iterator iterator = exportedPackages.iterator(); iterator.hasNext();) {
			importedPackages.addAll((Set) exportedAndUsedPackages.get(iterator.next()));
		}
		return importedPackages;
	}

	private void removePotentiallyConflictingImportsFrom(Set importedPackages) {
		Bundle systemBundle = OsgiUtils.getSystemBundleFrom(bundleContext);
		Set alreadyImported = OsgiUtils.aggregatePackageNamesEffectivelyImportedBy(systemBundle, bundleContext);
		importedPackages.removeAll(alreadyImported);
	}
}
